########################################################################
#  Pyfm - personal finance data-base
#  Copyright (C) 2007,8 Ehud Ben-Reuven
#  udi@benreuven.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation version 2.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
########################################################################
""" Library for Transaction objects
"""
from modellib import *
from lib.quoteinterface import getFXrate
import random


def gettransactions(transactions_filter,h=None,hs=None):
    transactions_filter['!?_SKIP_']=[] 
    if hs:
        transactions_filter['HoldingSource']=hs
    elif h:
        for hs in transactions_filter.setdefault('HoldingSource',[{}]):
            hs['Holding']=[h]
    return db.getallobjects(Transaction,transactions_filter)

def trnDBadd(line,file):
    line['FILE']=[file]
    nline=NLINE(file+'#'+str(random.randrange(10000000)))
    line['NLINE']=[nline]
    db.line_update(line)
    
def trnadd(t):
    """Return a new _MERGED_ transaction out of t and put it in DB"""
    line=t.dict()
    line['_LINE_INDEX_']=[] # Make sure we don't get confused with the line indexing generated by db.line_update
    line['Holding']=line['HoldingSource'][0]['Holding']
    line['HoldingSource']=[] # Let the DB find the HoldingSource by itself
    #line['OLINE']+=t.NLINE # FIXME should we use this or the code at the bottom???
    #line['_OTran_']=[t]
    fix_price_currency(line)
    trnDBadd(line,'_MERGED_')
    t.update_tag('_MTran_',line['Transaction'])
    # FIXME should we use this code or the line code???
    newt=line['Transaction'][0]
    newt.update_tag('_OTran_',[t])
    newt.update_tag('OLINE',t.NLINE)

    return newt

def trnXadd(t,line):
    trnDBadd(line,line['FILE'][0]+'_XTRAN_')
    if 'Transaction' in line:
        t.update_tag('_XTran_',line['Transaction'])
        newt=line['Transaction'][0]
        newt.update_tag('_XOTran_',[t])
        newt.update_tag('XOLINE',t.NLINE)

def trnprint(mtran,curdef=None):
    print strnprint(mtran,curdef),
    
def strnprint(mtran,curdef=None):
    date=mtran.DTSETTLE[0]
    msg=[str(date)]
    if mtran.DTTRADE and mtran.DTTRADE[0]!=mtran.DTSETTLE[0]:
        msg.append(str(mtran.DTTRADE[0]))
    
    if mtran.TRNTYPE: msg.append(str(mtran.TRNTYPE[0]))
    
    if mtran.UNITS: msg.append(str(mtran.UNITS[0]))
    unitprice,cur = getunitpricecur(mtran)
    if unitprice:
        computed='' if mtran.UNITPRICE else '_'
        msg.append(computed+str(unitprice))
    for tag in ['MARKUP','LOAD','COMMISSION','FEES','TAXES','WITHHOLDING','INTEREST']:
        if mtran[tag]:
            msg.append(str(mtran[tag][0]))
    
    computed=''
    if mtran.TOTAL:
        total=mtran.TOTAL[0]
    else:
        total=trntotal(mtran)
        if total!=None: total=TOTAL(total)
        computed='_'
    if total!=None:
        if cur and curdef and cur!=curdef:
            total*=getFXrate(date,cur,curdef)
    if total!=None:
        msg.append(computed+str(total)+scurdef(curdef,cur))
        
    if  trnmerged(mtran):
        msg.append( '('+','.join([o.HoldingSource[0].FILE[0] for o in mtran._OTran_])+')')
    else:
        msg.append(str(mtran.HoldingSource[0].FILE[0]))
    return ' '.join(msg)

def trnmerged(mtran):
    return mtran.HoldingSource[0]['FILE'][0]=='_MERGED_'

def gettransaction_filter(line,dtrs=None,dtre=None,default={'HoldingSource':[{'FILE':['_MERGED_']}]}):
    transaction_filter=getfilter(line,'Transaction',default=default)
    add2line(transaction_filter,'!DTSETTLE','SORT')

    if not dtrs: dtrs=line.get('DTSTART',[None])[0]
    if dtrs: add2line(transaction_filter,'>=DTSETTLE',dtrs)

    if not dtre: dtre=line.get('DTEND',[None])[0]
    if dtre: transaction_filter.setdefault('<DTSETTLE',[]).append(dtre)
    return transaction_filter

def trndist(t1,t2,minscore):
    """Return the "Distance between two transactions. The higher the better (up to 1000). Returen 0 if no match"""
    score=1000
    
    if t1==t2: return score
    score-=1
    # Transaction from different holdings never match
    if t1.HoldingSource[0].Holding[0]!=t2.HoldingSource[0].Holding[0]: return 0
    # transactions that share the same Financial Instituet Transaction ID
    # Note that this check is done before checking the source. This allows two transactions from the same source to be merged into one if
    # they have the same FITID or RELFITID (as long as it is from the same holding)
    # RELFITD canoverlap with FITID of another transaction when an import tool generate several lines for each imported line in the original statement
    # without beeing aware that the two splits are in the same holding
    fitid1s=t1.FITID+t1.OFITID+t1.RELFITID # OFITID is the FITID of the original transaction.
    fitid2s=t2.FITID+t2.OFITID+t2.RELFITID    
    if any(fitid in fitid1s for fitid in fitid2s): return score
    score-=1
    # transactions that have a conflicting FITID:
    # we know t1 and t2 dont have identical FITID, so now check if they have FITID from the same domain/subdomain
    # or from domain=None,subdomain=None
    #fitid2s_explode=[fitid.explode()[:2] for fitid in fitid2s]
    #if any(fitid.explode()[:2] in fitid2s_explode for fitid in fitid1s):
        #return 0
    for fitid1 in fitid1s:
        d1,s1,l1=fitid1.explode()
        for fitid2 in fitid2s:
            d2,s2,l2=fitid2.explode()
            if d1==d2 and s1==s2:
                score-=100
    if score<minscore: return 0
    # Any two transactions from the same source are different
    if t1.HoldingSource[0].FILE[0]==t2.HoldingSource[0].FILE[0]: return 0
            
    unitsdiff=0.
    totaldiff=0.
    
    if bool(t1.UNITS and t1.UNITS[0])!=bool(t2.UNITS and t2.UNITS[0]): return 0
    # Transfer may have UNITS without total
    if t1.UNITS and t1.UNITS[0] and t2.UNITS and t2.UNITS[0]:
        unitsdiff = abs(t1.UNITS[0]-t2.UNITS[0])
    if (unitsdiff>1 and (unitsdiff>100 or not t1.HoldingSource[0].Holding[0].isfixedprice())): 
        unitsdiff*=5
    score-=unitsdiff*10
    if score<minscore: return 0

    # Compute difference in dates (use day resolution)
    dtsettle1=t1.DTSETTLE[0].int()
    dtsettle2=t2.DTSETTLE[0].int()

    dttrade1=t1.DTTRADE[0].int() if t1.DTTRADE else dtsettle1
    dttrade2=t2.DTTRADE[0].int() if t2.DTTRADE else dtsettle2

    dtsettlediff=abs(dtsettle1-dtsettle2)
    dttradediff=abs(dttrade1-dttrade2)
    # In some cases one of the transactions has only the DTSETTLE or DTTRADE
    if (dtsettle1!=dttrade1 and dtsettle2==dttrade2) or (dtsettle1==dttrade1 and dtsettle2!=dttrade2):
        dttradediff=min(dtsettlediff,dttradediff)
        dtsettlediff=dttradediff

    if dtsettlediff>3:
        dtsettlediff*=10

    if dttradediff>2:
        dttradediff*=10
    score-=(dtsettlediff+dttradediff)/2
    if score<minscore: return 0
    
    # Computed TOTAL includes in it all the components (UNITS*UNITPRICE,INTEREST, LOAD, FEES,...)
    total1 = t1.TOTAL[0] if t1.TOTAL else trntotal(t1)
    total2 = t2.TOTAL[0] if t2.TOTAL else trntotal(t2)
    if total1!=None and total2!=None:
        unitprice1,currency1=getunitpricecur(t1)
        unitprice2,currency2=getunitpricecur(t2)
        if currency1!=currency2:
            assert currency1 and currency2 and t2.DTSETTLE[0]
            total2*=getFXrate(t1.DTSETTLE[0],currency2,currency1)
        totaldiff=100.*abs(total1-total2)/max(1,abs(total1),abs(total2))
    score-=totaldiff*50
    if score<minscore: return 0

    return score

def trnexpense(t):
    return sum(t[tag][0] for tag in ['LOAD','FEES','COMMISSION','WITHHOLDING','STATEWITHHOLDING','TAXES','PENALTY'] if t[tag])

def trntotal(t):
    """Compute the total sum of the money of a transaction that is supposed to move *to* the cashing account.
So for a buy this will be a negative number"""
    if not (t.UNITS or t.INTEREST or t.LOAD or t.FEES or t.COMMISSION or t.WITHHOLDING or t.STATEWITHHOLDING or t.TAXES):
        return
    total=0
    if t.UNITS:
        if t.UNITS[0]:
            unitprice,currency=getunitpricecur(t) # t.UNITPRICE can be missing (checking or transfer)
            if unitprice==None: return # In a transfer we can't compute total.
            total=unitprice*t.UNITS[0]
        if t.HoldingSource[0].Holding[0].ischecking(): return total
        total=-total
    if t.INTEREST: total+=t.INTEREST[0]
    return total-trnexpense(t)

def trnincome(t):
    """Compute the net income (interest-cost) generated in a transaction from knowing the total sum of the money of a transaction that is supposed to move *to* the cashing account. So for a buy this will be a negative number"""
    return t.TOTAL[0]+trnexpense(t) if t.TOTAL else None

def trnget(t):
    dtsettle=t.DTSETTLE[0]
    if t.DTTRADE:
        dttrade=t.DTTRADE[0]
    else:
        dttrade=dtsettle
    units= t.UNITS[0] if t.UNITS else 0
    price= t.UNITPRICE[0] if t.UNITPRICE else 0
    return dtsettle,dttrade,units,price

def trnskip(t):
    return getattr(t,'_SKIP_',False)
